﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace BacteriaSimulation.Simulation
{
    public delegate void SimulationUpdateEventHandler(SimulationUpdateEventArgs args);

    public enum SimulationState
    {
        RUNNING, PAUSED, STOPPED
    }

    public enum WALL_COLLISION_TYPE
    {
        RIGHT, TOP, BOTTOM, LEFT
    }

    public class Simulation
    {
        public static readonly int DEFAULT_GRID_ROW_COUNT = 8;
        public static readonly int DEFAULT_GRID_COLUMN_COUNT = 16;

        protected Random r = new Random();

        public Queue<SimulationCommand> CommandQueue { get; set; }
        public SimulationState State {get; set; }
        public int SimulationStepInterval { get; set; }
        public SimulationEnvironment Env { get; set; }
        public Double FreeMass { get; set; }

        protected List<Bacterium> RemovingBacteria { get; set; }
        protected List<Bacterium> AddingBacteria { get; set; }

        protected int cyanobacteriaCount = 0;
        protected int cannibalBacteriaCount = 0;

        protected IndexGrid grid;
        public IndexGrid Grid
        {
            get
            {
                return this.grid;
            }

            set 
            {
                this.grid = value;
            }
        }

        protected List<Bacterium> Bacteria
        {
            get;
            set;
        }

        public event SimulationUpdateEventHandler SimulationUpdate;

        public Simulation(SimulationEnvironment environment) :
            this(environment, DEFAULT_GRID_ROW_COUNT, DEFAULT_GRID_COLUMN_COUNT)
        {
        }

        public Simulation(SimulationEnvironment environment, int gridRowCount, int gridColumnCount)
        {
            this.CommandQueue = new Queue<SimulationCommand>();
            this.State = SimulationState.STOPPED;
            SimulationStepInterval = environment.SimulationStepInterval;
            this.Env = environment;
            this.Bacteria = new List<Bacterium>();
            RemovingBacteria = new List<Bacterium>();
            AddingBacteria = new List<Bacterium>();
            this.Grid = new IndexGrid(Env, gridRowCount, gridColumnCount);
            this.FreeMass = environment.TotalMass;
        }

        public void SimulationThreadStart()
        {
            try
            {
                this.State = SimulationState.RUNNING;
                Loop();
            }
            catch(Exception e)
            {
                System.Console.WriteLine(e.TargetSite + Environment.NewLine + e.StackTrace);
            }
        }

        public void Loop()
        {
            while(State != SimulationState.STOPPED)
            {
                SimulationCommand command = DequeueCommand();
                if(command != null)
                {
                    ExecuteCommand(command);
                }

                if(State == SimulationState.RUNNING)
                {
                    PerformSimulationStep();
                    OnSimulationUpdate();
                    Thread.Sleep(SimulationStepInterval);
                }
                else if (State == SimulationState.PAUSED)
                {
                    lock (CommandQueue)
                    {
                        Monitor.Wait(CommandQueue);
                    }
                    continue;
                }
                else if (State == SimulationState.STOPPED)
                {
                    break;
                }
            }
        }

        protected void PerformSimulationStep()
        {            // Update the list of bacteria, removing the dead bacteria and adding the new ones
            lock (this.Bacteria)
            {
                foreach (Bacterium removing in RemovingBacteria)
                {
                    RemoveBacterium(removing);
                    removing.NotifyDeath();
                }
                foreach (Bacterium adding in AddingBacteria)
                {
                    AddBacterium(adding);
                }
            }
            this.RemovingBacteria.Clear();
            this.AddingBacteria.Clear();

            foreach (Bacterium bact in Bacteria)
            {
                bact.PerformSimulationStep();
                UpdateBacteriumPosition(bact);
            }

        }

        protected void UpdateBacteriumPosition(Bacterium bact)
        {
            if (bact.State == BacteriumState.DEAD)
            {
                this.RemovingBacteria.Add(bact);
                return;
            }

            if (bact.Energy >= bact.MaxEnergy
                &&( ( cyanobacteriaCount - RemovingBacteria.Count() + AddingBacteria.Count() < Env.MaxBacteriaCount)
                    /*|| (bact.Strategy is CannibalBacteriumStrategy)*/
                  )
                )
            {
                // the Bacterium is dying
                bact.State = BacteriumState.DEAD;
                // add the bacterium to the list of the bacteria that will be removed
                this.RemovingBacteria.Add(bact);

                // duplicates the dying bacterium

                // Duplicates the bacterium
                Double leftXRand = r.NextDouble();
                Double leftYRand = r.NextDouble();
                Double rightXRand = r.NextDouble();
                Double rightYRand = r.NextDouble();
                Double energySplitFactor = r.NextDouble();
                Double energySplitFactorCompl = 1 - energySplitFactor;

                Vector2D leftSpeed = new Vector2D(Math.Sign(bact.Speed.XComponent) * leftXRand * Env.MaxSpeedComponent,
                    Math.Sign(bact.Speed.YComponent) * leftYRand * Env.MaxSpeedComponent);
                Vector2D rightSpeed = new Vector2D(Math.Sign(bact.Speed.XComponent) * rightXRand * Env.MaxSpeedComponent,
                    Math.Sign(bact.Speed.YComponent) * rightYRand * Env.MaxSpeedComponent);

                /////////////////////////////////////////////
                //Vector2D leftSpeed = Vector2D.GetRandomVector2D();
                //Vector2D rightSpeed = Vector2D.GetRandomVector2D();
                /////////////////////////////////////////////

                Bacterium leftBacterium = new Bacterium(bact.Position.Clone(), leftSpeed, bact.Energy * energySplitFactor, bact.Strategy, 
                    bact.MaxEnergy, bact.EnergyIncrement);

                SimulationPoint rightPosition = bact.Position.Clone();
                rightPosition.X += leftBacterium.Position.X + leftBacterium.Width + Env.Epsilon;
                rightPosition.Y += leftBacterium.Position.Y + leftBacterium.Height + Env.Epsilon;
                Bacterium rightBacterium = new Bacterium(bact.Position.Clone(), rightSpeed, bact.Energy * energySplitFactorCompl, bact.Strategy, 
                    bact.MaxEnergy, bact.EnergyIncrement);

                AddingBacteria.Add(leftBacterium);
                AddingBacteria.Add(rightBacterium);

                return;
            }

            bact.X += bact.Speed.XComponent;
            bact.Y += bact.Speed.YComponent;

            // Detect possible wall collision
            if (bact.X > (Env.Width - bact.Width))
            {
                bact.X = Env.Width - bact.Width - Env.Epsilon;
                bact.WallCollisionDetected(WALL_COLLISION_TYPE.RIGHT);
            }
            if (bact.X < 0)
            {
                bact.X = 0;
                bact.WallCollisionDetected(WALL_COLLISION_TYPE.LEFT);
            }
            if (bact.Y > (Env.Height - bact.Height))
            {
                bact.Y = (Env.Height - bact.Height - Env.Epsilon);
                bact.WallCollisionDetected(WALL_COLLISION_TYPE.TOP);
            }
            if (bact.Y < 0)
            {
                bact.Y = 0;
                bact.WallCollisionDetected(WALL_COLLISION_TYPE.BOTTOM);
            }

            this.Grid.Assign(bact);

            // detect possible collision with other bacteria
            if (Env.DetectBacteriaCollision)
            {
                // chek the cell to which the bact belongs
                foreach (Bacterium other in bact.Cell.Bacteria)
                {
                    //if (other == bact)
                    //    continue;
                    //SimulationPoint center = new SimulationPoint(bact.Position.X + bact.Width / 2, bact.Position.Y + bact.Height / 2);
                    //SimulationPoint otherCenter = new SimulationPoint(other.Position.X + other.Width / 2, other.Position.Y + other.Height / 2);

                    //// computes the next position
                    //Double bactInc = bact.Energy >= bact.MaxEnergy ? 0 : bact.WidthIncrement / 2;
                    //Double otherInc = other.Energy >= other.MaxEnergy ? 0 : other.WidthIncrement / 2;
                    //center.X += bact.Speed.XComponent + bactInc;
                    //center.Y += bact.Speed.YComponent + bactInc;
                    //otherCenter.X += other.Speed.XComponent + otherInc;
                    //otherCenter.Y += other.Speed.YComponent + otherInc;

                    //Boolean collision = SimulationPoint.Distance(center, otherCenter) <= (bact.Width / 2) + (other.Width / 2);

                    if (DetectCollision(bact, other))
                    {
                        bact.BacteriumCollisionDetected(other);
                        other.BacteriumCollisionDetected(bact);
                    }
                }

                // check the adjacent cells
                foreach (IndexGridCell cell in Grid.GetAdjacentCells(bact.Cell.RowIndex, bact.Cell.ColumnIndex))
                {
                    foreach(Bacterium other in cell.Bacteria)
                    {
                        if(DetectCollision(bact, other))
                        {
                            bact.BacteriumCollisionDetected(other);
                            other.BacteriumCollisionDetected(bact);
                        }
                    }
                }
            }
        }

        public static Boolean DetectCollision(Bacterium bacterium, Bacterium other)
        {
            if (other == bacterium)
            {
                return false;
            }
            SimulationPoint center = new SimulationPoint(bacterium.Position.X + bacterium.Width / 2, bacterium.Position.Y + bacterium.Height / 2);
            SimulationPoint otherCenter = new SimulationPoint(other.Position.X + other.Width / 2, other.Position.Y + other.Height / 2);

            // computes the next position
            Double bactInc = bacterium.Energy >= bacterium.MaxEnergy ? 0 : bacterium.WidthIncrement / 2;
            Double otherInc = other.Energy >= other.MaxEnergy ? 0 : other.WidthIncrement / 2;
            center.X += bacterium.Speed.XComponent + bactInc;
            center.Y += bacterium.Speed.YComponent + bactInc;
            otherCenter.X += other.Speed.XComponent + otherInc;
            otherCenter.Y += other.Speed.YComponent + otherInc;

            Boolean collision = SimulationPoint.Distance(center, otherCenter) <= (bacterium.Width / 2) + (other.Width / 2);

            return collision;
        }

        protected void ExecuteCommand(SimulationCommand command)
        {
            switch (command.CommandName)
            {
                case "pause":
                    {
                        this.State = SimulationState.PAUSED;
                        break;
                    }
                case "resume":
                    {
                        this.State = SimulationState.RUNNING;
                        break;
                    }
                case "stop":
                    {
                        this.State = SimulationState.STOPPED;
                        break;
                    }
                case "add":
                    {
                        AddBacterium(command as AddCommand);
                        break;
                    }
                case "remove":
                    {
                        RemoveBacterium(command as RemoveCommand);
                        break;
                    }
                case "killRange":
                    {
                        KillRange(command as KillRangeCommand);
                        break;
                    }
                case "dumpIndex":
                    {
                        DumpIndex();
                        break;
                    }
            }
        }

        protected void KillRange(KillRangeCommand cmd)
        {
            if (cmd != null)
            {
                foreach (Bacterium bact in Bacteria)
                {
                    if (bact.X >= cmd.X && bact.X <= cmd.X + cmd.Width
                        && bact.Y >= cmd.Y && bact.Y <= cmd.Y + cmd.Height)
                    {
                        bact.State = BacteriumState.DEAD;
                        RemovingBacteria.Add(bact);
                    }
                }
            }
        }

        protected void RemoveBacterium(RemoveCommand removeCmd)
        {
            if (removeCmd != null)
            {
                RemoveBacterium(removeCmd.Bacterium);
            }
        }

        protected void AddBacterium(AddCommand addCommand)
        {
            if (addCommand != null)
            {
                AddBacterium(addCommand.Bacterium);
            }
        }

        protected void AddBacterium(Bacterium adding)
        {
            Bacteria.Add(adding);
            if (adding.Strategy is CyanobacteriumSimulationStrategy)
            {
                cyanobacteriaCount += 1;
            }
            else if (adding.Strategy is CannibalBacteriumStrategy)
            {
                cannibalBacteriaCount += 1;
            }
            this.Grid.Assign(adding);
        }

        protected void RemoveBacterium(Bacterium removing)
        {
            this.Bacteria.Remove(removing);
            if (removing.Strategy is CyanobacteriumSimulationStrategy)
            {
                cyanobacteriaCount -= 1;
            }
            else if (removing.Strategy is CannibalBacteriumStrategy)
            {
                cannibalBacteriaCount -= 1;
            }
            this.Grid.Remove(removing);
        }

        protected void DumpIndex()
        {
            int total = 0;
            foreach (IndexGridCell cell in Grid.Cells)
            {
                System.Diagnostics.Debug.WriteLine("cell at [" + cell.RowIndex + ", " + cell.ColumnIndex + "] : " + cell.Bacteria.Count);
                total += cell.Bacteria.Count;
            }

            System.Diagnostics.Debug.WriteLine("Total: " + total);
        }

        protected SimulationCommand DequeueCommand()
        {
            try
            {
                bool lockAcquired = System.Threading.Monitor.TryEnter(CommandQueue);
                if (lockAcquired && CommandQueue.Count > 0)
                {
                    return CommandQueue.Dequeue();
                }
                else
                {
                    return null;
                }
            }
            finally
            {
                Monitor.Exit(CommandQueue);
            }
        }

        public void EnqueueCommand(SimulationCommand command)
        {
            try
            {
                bool lockAcquired = Monitor.TryEnter(CommandQueue);
                if (lockAcquired)
                {
                    CommandQueue.Enqueue(command);
                    Monitor.PulseAll(CommandQueue);
                }
            }
            finally
            {
                Monitor.Exit(CommandQueue);
                //Monitor.PulseAll(CommandQueue);
            }
        }

        protected void OnSimulationUpdate()
        {
            if(SimulationUpdate != null)
            {
                SimulationUpdateEventArgs args = new SimulationUpdateEventArgs(this.Bacteria, this.cyanobacteriaCount, this.cannibalBacteriaCount);
                SimulationUpdate(args);
                //this.State = SimulationState.PAUSED;
            }
        }
    }
}
